package main;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import problemSpace.*;

public class Evaluation {
	public static int TMAX = 100;
	// Gewichten voor kostenfunctie worden overschreven door de methode tryReadingWeights()
	public static double alpha1 = 10, alpha2 = 1, alpha3 = 10, alpha4 = 1, alpha5 = 20, alpha6 = 15;
	// HARDCONSTRPENALTY wordt ook overschreven door tryReadingWeights()
	public static int HARDCONSTRPENALTY = 100000000;
	public static boolean verbose = false,verboseHardConstr = false;
	public static boolean weightsread = false;
	// voor vehicle drivetimes, normale volumes vehicle voor volume te veel
	private static HashMap<String, Double> vehdrvmins, vehnrmvols;

	public static double calcScore(ConcretePlanning cp, boolean debugging) {
		verbose = debugging;
		verboseHardConstr = debugging;
		
		if (!weightsread) tryReadingWeights();
		if (verbose) vehdrvmins = new HashMap<String, Double>();
		// normale volumes vehicle voor volume te veel
		vehnrmvols = new HashMap<String, Double>();
		for (Vehicle veh : cp.vehicles)
			vehnrmvols.put(veh.vehicleCode, veh.normalVolume);
		double score = 0;
		// Voor alle orders
		for (Order o : cp.orders) {
			if (verbose) {
				System.out.println("******************************************************");
				System.out.println("ORDER " + o.orderCode + " (" + o.totalVolumeM3 + " m3)");
			}
			// doelf deel 1&2&3&4
			score += calcLatEarl(o,cp.output.resultList.get(cp.output.resultList.size() - 1));
			// doelf deel 5
			score += calcTimeLag(o,cp.output.resultList.get(cp.output.resultList.size() - 1),cp.vehicles);
			// drive times
			score += calcDriveTime(o,cp.output.resultList.get(cp.output.resultList.size() - 1));
		}
		// print drivetimes per vehicle
		if (verbose) {
			System.out.println("+++++++++++++++++++++++++++++++++++++++++++++++++++++++");
			for (Map.Entry<String, Double> entry : vehdrvmins.entrySet()) {
				System.out.println("Vehicle " + entry.getKey() + " drive time (min.): " + entry.getValue());
			}
		}
		// hard constraints
		score += checkHardConstraints(cp);
		// controleer per vehicle
		score += checkVehicleFeasability(cp);
		if (verbose) {
			System.out.println("Score: " + score);
			System.out.println("   ("+ cp.orders.size()+ " orders, "+cp.vehicles.size()+" vehicles, "+cp.stations.size()+" stations checked)");
			System.out.println("#######################################################");
		}
		return score;
	}

	public static Map<Order, Double> evaluateOrdersAndSort(ConcretePlanning s) {
		Map<Order, Double> map = new HashMap<Order, Double>();
		for (Order order : s.orders) {
			map.put(order, Evaluation.berekenOrderScore(order, s));
		}
		map = sortByValue(map);
		return map;
	}

	public static Order findWorst(Map<Order, Double> map, int index) {
		Object[] temp = map.keySet().toArray();
		index++;
		return (Order) temp[temp.length - index];
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static Map<Order, Double> sortByValue(Map<Order, Double> map) {
		List list = new LinkedList(map.entrySet());
		Collections.sort(list, new Comparator() {
			public int compare(Object o1, Object o2) {
				return ((Comparable) ((Map.Entry) (o1)).getValue()).compareTo(((Map.Entry) (o2)).getValue());
			}
		});

		Map result = new LinkedHashMap();
		for (Iterator it = list.iterator(); it.hasNext();) {
			Map.Entry entry = (Map.Entry) it.next();
			result.put(entry.getKey(), entry.getValue());
		}
		return result;

	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public static Map<ConcretePlanning, Double> sortPlannings(
			Map<ConcretePlanning, Double> map) {
		List list = new LinkedList(map.entrySet());
		Collections.sort(list, new Comparator() {
			public int compare(Object o1, Object o2) {
				return ((Comparable) ((Map.Entry) (o1)).getValue()).compareTo(((Map.Entry) (o2)).getValue());
			}
		});

		Map result = new LinkedHashMap();
		for (Iterator it = list.iterator(); it.hasNext();) {
			Map.Entry entry = (Map.Entry) it.next();
			result.put(entry.getKey(), entry.getValue());
		}
		return result;

	}

	/**
	 * Calculates score for a given order
	 * @return Average score per delivery
	 */
	public static double berekenOrderScore(Order order, ConcretePlanning cp) {
		// relatieve score : delen door aantal deliveries
		if (!weightsread) tryReadingWeights();
		double score = 0;
		if (verbose){
			System.out.println("******************************************************");
			System.out.println("ORDER " + order.orderCode + " (" + order.totalVolumeM3 + " m3)");
		}
		// doelf deel 1&2&3&4
		score += calcLatEarl(order,cp.output.resultList.get(cp.output.resultList.size() - 1));
		// doelf deel 5
		score += calcTimeLag(order,cp.output.resultList.get(cp.output.resultList.size() - 1),cp.vehicles);
		// doelf deel 6
		score += calcDriveTime(order,cp.output.resultList.get(cp.output.resultList.size() - 1));
		// hard constraints
		score += checkHardConstraintsPerOrder(order, cp);

		// tel het aantal deliveries
		int ndelis = 0;
		for (Delivery deli : cp.output.resultList.get(0).deliveries) {
			if (order.orderCode.equals(deli.orderCode))
				ndelis++;
		}
		if (ndelis == 0) score = Double.POSITIVE_INFINITY;
		else score /= Math.log(ndelis);
		
		if (verbose) System.out.println("Average score per delivery: " + (int) score + " ("+ndelis+" deliveries)");
		return score;
	}

	private static double calcLatEarl(Order o, Result pr) {
		double score = 0;
		double volumeTeVeel = 0;
		double preff = 0;
		Delivery firstdelivery = null;
		for (Delivery del : pr.deliveries) {
			if (o.orderCode.equals(del.orderCode)) {
				// alfa1&2
				if (firstdelivery == null || firstdelivery.unloadingTime.getTime() > del.unloadingTime.getTime())
					firstdelivery = del;
				// alfa3
				volumeTeVeel += (vehnrmvols.get(del.vehicleCode) - del.volume);
				// alfa4
				if (!o.preferredStationCode.equals(del.departureStationCode))
					preff++;
			}
		}
		if (firstdelivery == null) {
			if (verboseHardConstr) System.out.println("HARD CONSTRAINT: ORDER NIET GELEVERD " + o.orderCode);
			score += HARDCONSTRPENALTY;
		}
		// alfa1&2
		else {
			int drivingtime = Integer.MAX_VALUE;
			for (StationDuration sd : o.constructionYard.stDu) {
				if (firstdelivery.departureStationCode.equals(sd.stationCode)) {
					drivingtime = sd.drivingMinutes;
					break;
				}
			}
			if (verbose && drivingtime == Integer.MAX_VALUE)
				System.out.println("Evaluatie: DrivingMinutes voor delivery in stationdurations niet gevonden!");
			double diff1 = Math.abs(firstdelivery.loadingTime.getTime() + o.constructionYard.waitingMinutes * 60000 + drivingtime * 60000 - o.from.getTime());
			diff1 /= 60000;
			if (verbose && firstdelivery.loadingTime.getTime() + o.constructionYard.waitingMinutes * 60000 + drivingtime * 60000 - o.from.getTime() < 0) {
				System.out.println("Evaluatie: Order " + o.orderCode + ": " + diff1 + " minuten te vroeg! ");
			}

			// als pos. te laat - neg. te vroeg
			if (diff1 >= 0) {
				score += alpha1 * diff1;
				if (verbose && diff1 != 0.0) {
					System.out.println(" Lateness:  " + diff1);
					System.out.println(" Earliness: 0");
				}
			} else {
				score += alpha2 * (0 - diff1);
				if (verbose){
					System.out.println(" Lateness:  0");
					System.out.println(" Earliness: " + (0 - diff1));
				}
			}
		}
		// alfa3&4
		if (volumeTeVeel < 0)
			volumeTeVeel = 0; // te weinig volume wordt ergens anders bestraft, indien kleiner zet op 0
		score += alpha3 * volumeTeVeel + alpha4 * preff;
		if (verbose){
			if (volumeTeVeel != 0.0)
				System.out.println(" Volume te veel: " + volumeTeVeel + " m3");
			if (preff != 0.0)
				System.out.println(" Aantal keer niet naar prefferred station: " + (int) preff);
		}
		return score;
	}

	private static double calcTimeLag(Order o, Result pr, ArrayList<Vehicle> vehicles) {
		double score = 0;
		ArrayList<Delivery> od = new ArrayList<Delivery>();
		// selecteer deliveries van order
		for (Delivery del : pr.deliveries) {
			if (o.orderCode.equals(del.orderCode))
				od.add(del);
		}
		// sorteer op unloading time
		Collections.sort(od, new Comparator<Delivery>() {
			@Override
			public int compare(Delivery a, Delivery b) {
				long t1 = a.unloadingTime.getTime();
				long t2 = b.unloadingTime.getTime();
				return (int) (t1 - t2);
			}
		});
		for (int i = 1; i < od.size(); i++) {
			Delivery earlier = od.get(i - 1);
			Delivery current = od.get(i);
			// end = einde vorige
			long end = earlier.unloadingTime.getTime() + (long) (earlier.volume / o.requiredDischargeM3PerHour * 60 * 60000);
			long diff = current.unloadingTime.getTime() - end;
			if (diff < 0) {
				if (verbose)
					System.out.println("Evaluatie: error bij timelag: negatief");
				diff = 0;
			}
			if (verbose)
				if (diff > 60 * 60000){
					System.out.println("Evaluatie: Order " + o.orderCode+ ": Timelag tussen twee deliveries is " + (diff / 60000) + " minuten!");
				}
			score += (diff / 60000);
		}
		if (score != 0.0)
			if (verbose)
				System.out.println(" TimeLag (min.): " + score);
		score *= alpha5;
		return score;
	}

	private static double calcDriveTime(Order order, Result pr) {
		double score = 0;
		// selecteer alle deliveries
		for (Delivery del : pr.deliveries) {
			if (del.orderCode.equals(order.orderCode)) {
				// rijtijden naar de werf
					for (StationDuration sd : order.constructionYard.stDu) {
						if (sd.stationCode.equals(del.departureStationCode)) {
							// score updaten
							score += sd.drivingMinutes;
							// voor vehicle stats
							if (verbose) {
								double vehdrivmin = sd.drivingMinutes;
								if (vehdrvmins.containsKey(del.vehicleCode))
									vehdrivmin += vehdrvmins.get(del.vehicleCode);
								vehdrvmins.put(del.vehicleCode, vehdrivmin);
							}
							break;
						}
					}
					for (StationDuration sd : order.constructionYard.stDu) {
						if (sd.stationCode.equals(del.returnStationCode)) {
							score += sd.drivingMinutes;
							// voor vehicle stats
							if (verbose) {
								double vehdrivmin = sd.drivingMinutes;
								if (vehdrvmins.containsKey(del.vehicleCode))
									vehdrivmin += vehdrvmins.get(del.vehicleCode);
								vehdrvmins.put(del.vehicleCode, vehdrivmin);
							}
							break;
						}
					}
			}
		}
		if (verbose)
			System.out.println(" Drive time (min.): " + score);
		score *= alpha6;
		return score;
	}

	private static double checkHardConstraints(ConcretePlanning cp) {
		double score = 0;
		ArrayList<Delivery> result = cp.output.resultList.get(cp.output.resultList.size() - 1).deliveries;
		for (Delivery d : result) {
			for (Order o : cp.orders) {
				if (d.orderCode.equals(o.orderCode)) {
					// 100 min tss laden en leveren in millis
					long diff = d.unloadingTime.getTime() - d.loadingTime.getTime();
					// diff + tijd om uit te laden
					diff += ((d.volume / o.requiredDischargeM3PerHour) * 60 * 60 * 1000);
					// check de 100 min - 5 min waiting time
					if ((diff / 60000) > TMAX+5) {
						if(verboseHardConstr) System.out.println("HARD CONSTRAINT: TMAX , Order " + d.orderCode + " Vehicle " + d.vehicleCode);
						score += HARDCONSTRPENALTY;
					}
					// prohib. vehicles
					for (String veh : o.prohibitedTypes) {
						if (veh.equals(d.vehicleCode)) {
							if(verboseHardConstr) System.out.println("HARD CONSTRAINT: PROHIB. VEH. " + d.vehicleCode + " Order " + o.orderCode);
							score += HARDCONSTRPENALTY;
						}

					}
					// checkt de pumpline length
					for (Vehicle veh : cp.vehicles) {
						if (veh.vehicleCode.equals(d.vehicleCode)) {
							if (veh.getPumpLineLengthM() < o.getPumpLineLengthRequiredM()) {
								if(verboseHardConstr) System.out.println("HARD CONSTRAINT: PUMP LINE LENGTH, " + "Order " + o.orderCode + " Vehicle " + veh.vehicleCode);
								score += HARDCONSTRPENALTY;
							}
							break;
						}
					}
					break;
				}
			}
		}
		// check alle orders voldaan
		for (Order o : cp.orders) {
			double vol = o.totalVolumeM3;
			for (Delivery d : result) {
				if (o.orderCode.equals(d.orderCode))
					vol -= d.volume;
			}
			if (vol > 0) {
				if(verboseHardConstr) System.out.println("HARD CONSTRAINT: TE WEINIG VOLUME GELEVERD ("+(int)vol+"m3), Order " + o.orderCode +" ("+(int)o.totalVolumeM3+"m3)");
				score += HARDCONSTRPENALTY;
			}
		}
		return score;
	}

	private static double checkVehicleFeasability(ConcretePlanning cp) {
		double score = 0;
		ArrayList<Delivery> result = cp.output.resultList.get(cp.output.resultList.size() - 1).deliveries;
		// per vehicle
		for (Vehicle vehicle : cp.vehicles) {
			ArrayList<Delivery> delisperveh = new ArrayList<Delivery>();
			for (Delivery delivery : result) {
				if (delivery.vehicleCode.equals(vehicle.vehicleCode))
					delisperveh.add(delivery);
			}
			// sorteer op unloading time
			Collections.sort(delisperveh, new Comparator<Delivery>() {
				@Override
				public int compare(Delivery a, Delivery b) {
					long t1 = a.unloadingTime.getTime();
					long t2 = b.unloadingTime.getTime();
					return (int) (t1 - t2);
				}
			});
			Delivery preceedingDeli = null;
			Order preceedingOrder = null;
			if (delisperveh.size() > 0){
				preceedingDeli = delisperveh.remove(0);
				for(Order o : cp.orders){
					if(preceedingDeli.orderCode.equals(o.orderCode)){
						preceedingOrder = o;
						break;
					}
						
				}
			}
			for (Delivery delivery : delisperveh) {
				//bereken unloading gedaan van laatste deli/order	
				int returnminutes = 0;
				if(preceedingOrder!=null){
					for(StationDuration sd : preceedingOrder.constructionYard.stDu){
						if (preceedingDeli.returnStationCode.equals(sd.stationCode)){
							returnminutes = sd.drivingMinutes;
							break;
						}
					}
					if(returnminutes==0)System.err.println("checkVehicleFeasability: return minutes not found");
				}
				if(!preceedingDeli.returnStationCode.equals(delivery.departureStationCode)){
					if(verboseHardConstr) {
						System.out.println("HARD CONSTRAINT: Return station of delivery does not match next loading station");
					}
					score += HARDCONSTRPENALTY;
				}
				for(Order o : cp.orders){
					if(delivery.orderCode.equals(o.orderCode)){
						long unloading = (long) (preceedingDeli.volume/preceedingOrder.requiredDischargeM3PerHour*60*60*1000);
						long unloadingdone = preceedingDeli.unloadingTime.getTime()+unloading+(returnminutes*60000);
						long diff = delivery.loadingTime.getTime() - unloadingdone;
						if (diff < 0) {
							if(verboseHardConstr) {
								System.out.println("HARD CONSTRAINT: TIMELAG BETWEEN DELIVERIES NEGATIVE "+diff/60000);
							}
							score += HARDCONSTRPENALTY;
						}
						preceedingOrder = o;
						break;
					}
				}
				preceedingDeli = delivery;
			}
			
		}
		//per station
		for(Station s : cp.stations){
			ArrayList<Delivery> delispersta = new ArrayList<Delivery>();
			for (Delivery delivery : result) {
				if (delivery.departureStationCode.equals(s.stationCode))
					delispersta.add(delivery);
			}
			// sorteer op unloading time
			Collections.sort(delispersta, new Comparator<Delivery>() {
				@Override
				public int compare(Delivery a, Delivery b) {
					long t1 = a.loadingTime.getTime();
					long t2 = b.loadingTime.getTime();
					return (int) (t1 - t2);
				}
			});
			Delivery preceedingDeli = null;
			if (delispersta.size() > 0){
				preceedingDeli = delispersta.remove(0);
			}
			for (Delivery delivery : delispersta) {
				if(preceedingDeli.loadingTime.getTime()+5*60000>delivery.loadingTime.getTime()){
					if(verboseHardConstr) {
						System.out.println("HARD CONSTRAINT: Two deliveries overlap at the loading station!");
					}
					score += HARDCONSTRPENALTY;
				}
				preceedingDeli = delivery;
			}
		}
		return score;
	}

	private static double checkHardConstraintsPerOrder(Order order, ConcretePlanning cp) {
		double score = 0;
		ArrayList<Delivery> result = cp.output.resultList.get(cp.output.resultList.size() - 1).deliveries;
		for (Delivery d : result) {
			if (d.orderCode.equals(order.orderCode)) {
				// 100 min tss laden en leveren in millise
				long diff = d.unloadingTime.getTime() - d.loadingTime.getTime();
				// diff + tijd om uit te laden
				diff += ((d.volume / order.requiredDischargeM3PerHour) * 60 * 60 * 1000);
				// check de 100 min -5min loading time
				if ((diff / 60000) > TMAX+5) {
					if(verboseHardConstr) System.out.println("HARD CONSTRAINT: TMAX , Order " + d.orderCode + " Vehicle " + d.vehicleCode);
					score += HARDCONSTRPENALTY;
				}
				// prohib. vehicles
				for (String veh : order.prohibitedTypes) {
					if (veh.equals(d.vehicleCode)) {
						if(verboseHardConstr) System.out.println("HARD CONSTRAINT: PROHIB. VEH. " + d.vehicleCode + " Order " + order.orderCode);
						score += HARDCONSTRPENALTY;
					}

				}
				// checkt de pumpline length
				for (Vehicle veh : cp.vehicles) {
					if (veh.vehicleCode.equals(d.vehicleCode)) {
						if (veh.getPumpLineLengthM() < order.getPumpLineLengthRequiredM()) {
							if(verboseHardConstr) System.out.println("HARD CONSTRAINT: PUMP LINE LENGTH, " + "Order " + order.orderCode + " Vehicle " + veh.vehicleCode);
							score += HARDCONSTRPENALTY;
						}
						break;
					}
				}
				break;
			}
		}
		// check alle orders voldaan
		double vol = order.totalVolumeM3;
		for (Delivery d : result) {
			if (order.orderCode.equals(d.orderCode))
				vol -= d.volume;
		}
		if (vol > 0) {
			if(verboseHardConstr) System.out.println("HARD CONSTRAINT: TE WEINIG VOLUME GELEVERD, Order " + order.orderCode);
			score += HARDCONSTRPENALTY;
		}
		return score;
	}

	private static void tryReadingWeights() {
		try {
			FileReader fi = new FileReader("weights.properties");
			BufferedReader br = new BufferedReader(fi);
			alpha1 = readDouble(br.readLine());
			alpha2 = readDouble(br.readLine());
			alpha3 = readDouble(br.readLine());
			alpha4 = readDouble(br.readLine());
			alpha5 = readDouble(br.readLine());
			alpha6 = readDouble(br.readLine());
			TMAX = (int) readDouble(br.readLine());
			br.close();
			fi.close();
			System.out.println("Evaluatie: weights.properties ingelezen");
			weightsread = true;
		} catch (FileNotFoundException fnfe) {
			System.out.println("Evaluatie: weights.properties not found");
			fnfe.printStackTrace();
		} catch (IOException ioe) {
			ioe.printStackTrace();
		}
	}

	private static double readDouble(String line) {
		String s = line.substring(line.indexOf("=") + 1);
		return Double.parseDouble(s);
	}
}
